Skip to content

Fix phpstan/phpstan#7280: array_reduce could infer the precise type of the $initial argument within the callable and in the return type#5168

Closed
phpstan-bot wants to merge 2 commits intophpstan:2.1.xfrom
phpstan-bot:create-pull-request/patch-28evfgd
Closed

Fix phpstan/phpstan#7280: array_reduce could infer the precise type of the $initial argument within the callable and in the return type#5168
phpstan-bot wants to merge 2 commits intophpstan:2.1.xfrom
phpstan-bot:create-pull-request/patch-28evfgd

Conversation

@phpstan-bot
Copy link
Copy Markdown
Collaborator

Summary

array_reduce now infers the precise type of the $initial argument as the $carry parameter type within the callback. Previously, $carry was typed as mixed (from the function signature), which when intersected with the closure's native type hint gave only broad types like array or non-empty-array. Now, constant array shapes like array{starts: array{}, ends: array{}} are preserved inside the callback.

Changes

  • Added src/Type/Php/ArrayReduceCallbackClosureTypeExtension.php — a new FunctionParameterClosureTypeExtension that overrides the callback parameter types for array_reduce:
    • The carry parameter gets the generalized initial argument type (preserves array shapes, generalizes scalar literals like 0int)
    • The value parameter gets the array's iterable value type
  • Added tests/PHPStan/Analyser/nsrt/bug-7280.php — regression test covering:
    • Constant array shape initial values (the primary use case from the issue)
    • Scalar initial values (int, string)
    • No initial value (defaults to null)
    • Arrow functions

Root cause

The function signature for array_reduce in functionMap.php declares the callback as callable(mixed,mixed):mixed. This meant the carry parameter had no type information beyond mixed, so PHPStan could only use the closure's native type hints. By implementing a FunctionParameterClosureTypeExtension, the initial argument's type is now propagated to the carry parameter, which is then intersected with the native type hint to produce precise types inside the callback body.

Test

The regression test verifies that:

  • $carry inside a callback with initial: ['starts' => [], 'ends' => []] is typed as array{starts: array{}, ends: array{}} (was non-empty-array)
  • Scalar initial values like 0 and '' are generalized to int and string (no over-narrowing)
  • Missing initial value correctly types carry as null

Fixes phpstan/phpstan#7280

- Added ArrayReduceCallbackClosureTypeExtension implementing FunctionParameterClosureTypeExtension
- The extension provides the initial argument type (generalized to remove literals) as the carry parameter type
- The array's value type is provided as the value parameter type
- New regression test in tests/PHPStan/Analyser/nsrt/bug-7280.php

Closes phpstan/phpstan#7280
$result1 = array_reduce(
['test1', 'test2'],
static function (array $carry, string $value): array {
assertType("array{starts: array{}, ends: array{}}", $carry);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this type is wrong. it only is correct for the first invocation when initial is used.

in this example, I think we need to aim for `assertType("array{starts: array, ends: array}", $carry);

$result3 = array_reduce(
[1, 2, 3],
static function (?int $carry, int $value): int {
assertType('null', $carry);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
assertType('null', $carry);
assertType('int|null', $carry);

Copy link
Copy Markdown
Contributor

@VincentLanglet VincentLanglet left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is wrong

assertType("array{starts: array{}, ends: array{}}", $carry);

and

assertType('null', $carry);

are only true for the first iteration but not for the following one.

The inferred type should be closer to

assertType("array{starts: array, ends: array}", $carry);

and

assertType('int|null', $carry);

The carry parameter type should be the union of the initial value type
and the callback's return type, since after the first iteration $carry
receives the callback's return value, not the initial value. Previously,
only the initial type was used, which was incorrect for subsequent
iterations (e.g., array{starts: array{}, ends: array{}} was wrong when
the callback returns a modified array, and null was wrong when the
callback returns int).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@phpstan-bot
Copy link
Copy Markdown
Collaborator Author

Done. Here's what I changed:

Problem: The $carry parameter type in array_reduce callbacks only accounted for the initial value type, which was wrong for iterations after the first one (where $carry receives the callback's return value).

Fix in ArrayReduceCallbackClosureTypeExtension.php: When the callback is a closure/arrow function with a declared return type, union that return type with the generalized initial type. This correctly represents that $carry can be either the initial value (first iteration) or the callback's return value (subsequent iterations).

Test changes:

  • Test 1: array{starts: array{}, ends: array{}}array (union of initial shape with array return type)
  • Test 3: nullint|null (union of null initial with int return type)

@staabm staabm deleted the create-pull-request/patch-28evfgd branch March 29, 2026 19:55
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants